Skip to content

Add expense activity history foundation - #10237

Closed
Prangshuman Das (t-prda) wants to merge 29 commits into
mainfrom
feature/expense-activity-log-foundation
Closed

Add expense activity history foundation#10237
Prangshuman Das (t-prda) wants to merge 29 commits into
mainfrom
feature/expense-activity-log-foundation

Conversation

@t-prda

@t-prda Prangshuman Das (t-prda) commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a single append-only expense activity table whose source reference moves from the active report to the posted report while its logical subject remains stable
  • record Created, Submitted, Resubmitted, Recalled, Approved, Rejected, Reopened by Approver, and Posted lifecycle events with actor, role, comments, financial snapshots, categories, and receipt count
  • expose read-only report-scoped and Expense User role-scoped APIs, plus active/posted/manager factboxes and the ActivityLog capability
  • add source deletion cleanup, posting reassignment, unit/API/posting tests, and a cleanup-safe activity API E2E scenario

Validation

  • the AL tests passed in a manually published local environment before rebasing
  • active report, posted report, submitter history, and approver history APIs were exercised against persisted local scenarios
  • unscoped activity and user history without historyActorRole were verified to fail closed
  • API integration tests are listed in the disabled-test manifest because the current CI environment does not support their required API isolation

Fixes AB#644172

Related: AB#639704

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
@t-prda
Prangshuman Das (t-prda) requested review from a team August 13, 2026 15:50
@github-actions github-actions Bot added AL: Apps (W1) Add-on apps for W1 Integration GitHub request for Integration area labels Aug 13, 2026
@github-actions github-actions Bot added this to the Version 29.0 milestone Aug 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Style}$

The changed permission-set source file is named ExpenseManagementObjects.permissionset.al, but the style contract requires <ObjectName>.<ObjectType>.al with the exact AL object type segment. Keeping permissionset lowercase weakens the file-system-to-object mapping that grep, tooling, and code navigation rely on; rename it to ExpenseManagementObjects.PermissionSet.al.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
@t-prda
Prangshuman Das (t-prda) requested a review from a team as a code owner August 13, 2026 16:07
Comment thread src/Apps/W1/ExpenseAgent/test/src/API/ExpenseAgentAPIScenarios.Codeunit.al Outdated
Comment thread src/Apps/W1/ExpenseAgent/test/src/API/ExpenseAgentAPIScenarios.Codeunit.al Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Apply partial-record reads and mediate activity table access through the API and factbox pages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
@t-prda

Copy link
Copy Markdown
Contributor Author

The filename comment refers to a pre-existing repository file, ExpenseManagementObjects.permissionset.al, that this PR only updates. Renaming that shared permission-set file would be unrelated churn, so it is intentionally left unchanged.

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Interfaces}$

Capability-specific behaviour is still selected with a case over the Expense Capability enum. In this PR, adding ActivityLog required synchronized edits to both the enum and IsEnabled, which is the variant-dispatch anti-pattern this rule calls out. Model the capability variants as enum-backed interface implementations and dispatch through an interface variable so future capabilities do not require editing the consumer codeunit.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

Remove retention registration from the foundation and track install, upgrade, refresh, permission, and audit semantics separately.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
Select query-backed keys, lock wire-token errors, avoid UI handlers, and restore setup backups before disabled-isolation tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Performance}$

ProcessExpenseReportLines calls CreateSalesDocument for every posted expense line, and CreateSalesDocument re-runs SalesHeader.FindFirst on the persistent Sales Header table each time to locate an open invoice for the same customer/currency/posting-date combination. That creates an N+1 lookup pattern during posting; cache or pre-group the target sales document (and its next line number) for the duration of the loop instead of querying Sales Header per line.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Security}$

The new audit logging trusts caller-supplied expense-user numbers for approval actions and records them as the actor without first binding them to the report's configured approver. In the internal Approve/Reject overloads, CheckApproverPermissions(ExpenseUser) only checks the global Can Approve flag on the supplied Expense User, unlike the session-based overload which additionally validates the caller against Expense Approval Setup/Default Approver No. for that specific report. An agent caller that can reach these internal actions can therefore attribute an approval or rejection to any user with Can Approve set, even one not configured as the approver for this report. Validate the supplied approver against the report's approval setup before state changes, and derive the logged actor from that validated approver rather than the raw request parameter.

Agent judgement — not directly backed by a BCQuality knowledge article.

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Style}$

The changed file src/Apps/W1/ExpenseAgent/app/src/Permissions/ExpenseManagementObjects.permissionset.al uses a lowercase object-type suffix. The style rule for AL source files is <ObjectName>.<ObjectType>.al, so this permission set file should be renamed to ExpenseManagementObjects.PermissionSet.al to preserve the filesystem-to-object mapping that search and tooling rely on.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Testing}$

ExpenseActivityLogAPITest.E2EActivityLogScenario uses a modal-page handler that always invokes OK without any enqueue/dequeue expectation, and the test does not prove which Expenses dialog was shown. That matches the hardcoded-handler anti-pattern: the scenario can stay green even if the wrong UI interaction occurs, so it does not reliably verify the headless UI path it claims to cover.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Style}$

This PR modifies src/Apps/W1/ExpenseAgent/app/src/Permissions/ExpenseManagementObjects.permissionset.al, but the file name does not follow the <ObjectName>.<ObjectType>.al convention. The object is a permissionset, so the type segment should be PermissionSet (ExpenseManagementObjects.PermissionSet.al), otherwise symbol-to-file lookup and tooling stay inconsistent for a file this PR is already touching.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

Grant direct read through Expense Mgmt. Read like the other factbox source tables.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Error\ Handling}$

if TrySendReimbursementNotification(PostedExpenseReportHeader) then; technically enables try semantics, but the empty branch discards false and never handles the captured error. That silently swallows reimbursement-notification failures during posting; consume the Boolean result in a real branch and handle the failure immediately while the last-error state is still available.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

Comment on lines +302 to +307
var
SubmitterExpenseUser: Record "Expense User";
ApproverExpenseUser: Record "Expense User";
ExpenseReportHeader: Record "Expense Report Header";
ExpenseActivityLogEntry: Record "Expense Activity Log Entry";
ExpenseReportApprovalMgt: Codeunit "Expense Report Approval Mgmt";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Testing}$

ReopeningApprovedReportIsLogged uses CurrentUserExpenseUser later in the method, but that record is not declared in the method's var block. The new test codeunit will not compile until the local variable is added.

Suggested change
var
SubmitterExpenseUser: Record "Expense User";
ApproverExpenseUser: Record "Expense User";
ExpenseReportHeader: Record "Expense Report Header";
ExpenseActivityLogEntry: Record "Expense Activity Log Entry";
ExpenseReportApprovalMgt: Codeunit "Expense Report Approval Mgmt";
var
SubmitterExpenseUser: Record "Expense User";
ApproverExpenseUser: Record "Expense User";
CurrentUserExpenseUser: Record "Expense User";
ExpenseReportHeader: Record "Expense Report Header";
ExpenseActivityLogEntry: Record "Expense Activity Log Entry";
ExpenseReportApprovalMgt: Codeunit "Expense Report Approval Mgmt";

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Accessibility}$

This promoted navigation group is captioned 'Expense', but the actions inside operate on the current expense report. Using the entity name 'Expense Report' matches the standard promoted-group naming guidance and aligns this page with the posted expense report page added in the same change.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

                Caption = 'Expense Report';

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Accessibility}$

The promoted action group uses the nonstandard caption 'Approval'. BC's standard workflow group name is 'Approve', so keeping 'Approval' here makes this action bar diverge from the platform's established document-page vocabulary.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

                Caption = 'Approve';

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Accessibility}$

This promoted navigation group is captioned 'Expense', but the actions inside are tied to the current expense report record. The standard pattern is to use an entity-named group, so captioning it 'Expense Report' keeps the ribbon consistent with native BC pages.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

                Caption = 'Expense Report';

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Privacy}$

The new reimbursement-notification flow sends the employee email in the "On-Behalf-Of" header and the posted expense report id to the external Expense Agent service, but this path never checks the Expense Agent privacy notice before calling SendReimbursementNotification. The integration should confirm or verify its own notice on this request path before transferring data externally.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Privacy}$

The new Expense Report Header table relies on table-level DataClassification and leaves 25 stored fields without explicit field classifications, including "Expense User No.", "Expense User Name", "Description", and "Spend Request No.". Table-level classification does not satisfy field-level privacy classification requirements, so these personal and customer-bearing fields ship under-classified.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Privacy}$

The new Posted Expense Report Header table also relies on table-level DataClassification and leaves 25 stored fields without explicit field classifications, including "Expense User No.", "Expense User Name", "Description", and "Spend Request No.". Field-level classifications are still required for these persisted values.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

Remove the API page from human Expense Management object permissions; the FactBox remains available there.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
ExpenseActivityLogEntry.Get(EntryNumber);
ExpenseActivityLogEntry."Source Table ID" := Database::"Posted Expense Report Header";
ExpenseActivityLogEntry."Source Record System ID" := PostedExpenseReportHeader.SystemId;
ExpenseActivityLogEntry.Modify(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Data\ Integrity}$

"Expense Activity Log Entry" declares OnModify/OnRename triggers that Error('Activity log entries cannot be modified.') to enforce an append-only audit trail. However, ReassignExpenseReportEntriesToPosted calls ExpenseActivityLogEntry.Modify(false) to rewrite "Source Table ID"/"Source Record System ID" on existing entries, which deliberately bypasses that trigger. This is a cross-cutting concern (data-integrity guarantee vs. security/auditability expectation): the table's public contract advertises immutable entries, but any code holding tabledata permission can silently rewrite an entry's source identity the same way, undermining the audit trail's tamper-evidence. Consider exposing a dedicated, narrowly-scoped reassignment API (e.g. a SQL-level or trigger-aware update path with its own validation) rather than relying on Modify(false) to route around the immutability guard, or documenting explicitly that the guard only protects against direct user/API edits and not internal reassignment.

Agent judgement — not directly backed by a BCQuality knowledge article.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d9178a5c-c5ff-43a4-b9cb-877ec18618fa
@t-prda

Copy link
Copy Markdown
Contributor Author

Superseded by a replacement PR from the same branch to reset stale review and CI history after the design was finalized.

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Interfaces}$

This change adds another branch to the existing case Capability of dispatcher in "Expense Capabilities Provider".IsEnabled. Adding the new ActivityLog capability now requires editing the consumer again instead of plugging in a new implementation behind an interface, which is the enum-dispatch anti-pattern the interfaces guidance warns against.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Privacy}$

The new reimbursement-notification path sends data to the external Expense Agent service without checking the custom privacy notice on the sending path. Both TrySendReimbursementNotification() and CheckAndSendReimbursementNotification() proceed from configuration checks straight to EAHttpClient.SendReimbursementNotification(ExpenseUser2."E-mail", PostedExpReportHeader.SystemId), so the expense user's email address and report identifier can be transferred even though this code never calls ConfirmPrivacyNoticeApproval(...) or GetPrivacyNoticeApprovalState(...) before the request.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Style}$

UpdateReportLines builds a user-facing confirmation from CanModifyLinesQst, but the routine is driven by Rec.FieldName(...) values. That leaks developer identifiers into the prompt instead of translated captions, even though the label comment says %1 = Field Caption. Use a user-facing FieldCaption(...) value for the message and keep any internal selector separate.

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Style}$

EntryRecIDLbl contains two placeholders (%1 %2) but has no Comment, so translators cannot tell that ShowDimensions() passes the table caption and document number. Add a placeholder comment at the declaration.

Suggested fix (apply manually — could not be anchored as a one-click suggestion):

        EntryRecIDLbl: Label '%1 %2', Comment = '%1 = Table Caption, %2 = Document No.', Locked = true;

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Web\ Services}$

This page is keyed by SystemId, but the expenseReportLines navigation property links children by "Document No." = field("No.") instead of the report's SystemId. That makes the navigation property depend on a business key rather than the API contract's stable GUID. Add a parent-SystemId foreign key to the line API/table and link the part with field(SystemId).

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Web\ Services}$

This page is keyed by SystemId, but the expenses, expenseReports, approverView, and expenseApprovalSetup navigation properties still link child APIs on field("No."). That creates a second identity scheme for navigation, so those child links depend on a renamable business key instead of the API contract's stable GUID. Add SystemId-based foreign keys on the child APIs and switch these SubPageLink clauses to field(SystemId).

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟠\ High\ Severity\ —\ Web\ Services}$

This page is keyed by SystemId, but the postedExpenseReportLines navigation property links children by "Document No." = field("No.") instead of the posted report's SystemId. That makes the navigation property depend on a business key rather than the API contract's stable GUID. Add a parent-SystemId foreign key to the posted line API/table and link the part with field(SystemId).

Knowledge:

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AL: Apps (W1) Add-on apps for W1 Integration GitHub request for Integration area

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant